// $Id: channel.c 84 2011-04-24 09:23:49Z cedric.shih@gmail.com $
/*
 * Copyright (C) 2011 Cedric Shih <cedric.shih@gmail.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>

#include "log.h"
#include "socket.h"
#include "buffer.h"

#include "channel.h"

struct niochannel *niochannel_new(struct nioreactor *reactor,
		struct niobuffer_factory *factory, void *cbarg)
{
	struct niochannel *channel;

	niodebug("constructing");

	if (!(channel = calloc(1, sizeof(*channel)))) {
		nioerror("failed to calloc");
		goto exception;
	}

	TAILQ_INIT(&channel->handlers);
	TAILQ_INIT(&channel->replaced);

	memcpy(&channel->factory, factory, sizeof(channel->factory));
	channel->factory_arg = cbarg;

	return channel;

exception:
	niochannel_free(channel);
	return NULL;
}

void niochannel_free(struct niochannel *channel)
{
	struct niohandler_item *item;

	niodebug("destructing: %p", channel);

	if (!channel) {
		return;
	}

	while ((item = TAILQ_FIRST(&channel->replaced))) {
		TAILQ_REMOVE(&channel->replaced, item, entry);
		niohandler_free(item);
	}

	while ((item = TAILQ_FIRST(&channel->handlers))) {
		TAILQ_REMOVE(&channel->handlers, item, entry);
		niohandler_free(item);
	}

	free(channel);
}

inline struct niobuffer *niochannel_buffer(struct niochannel *channel)
{
	return channel->buffer;
}

inline enum niochannel_direction niochannel_direction(struct niochannel *channel)
{
	return channel->direction;
}

int niochannel_keep_alive(struct niochannel *channel)
{
	int rc;

	if (!channel) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("keeping channel alive");

	channel->keep_alive = 1;
	rc = 0;

finally:
	return rc;
}

inline int niochannel_is_kept_alive(struct niochannel *channel)
{
	return channel->keep_alive;
}

int niochannel_add_first(struct niochannel *channel, const char *name,
		struct niohandler *handler, void *obj)
{
	int rc;
	struct niohandler_item *item;

	if (!channel || !name || !handler) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("adding first: %s", name);

	if (niochannel_find(channel, name)) {
		nioerror("already exist: %s", name);
		rc = NIO_EEXIST;
		goto finally;
	}

	if (!(item = niohandler_new(name, handler, obj))) {
		nioerror("failed to create handler");
		rc = NIO_ENOMEM;
		goto finally;
	}

	TAILQ_INSERT_HEAD(&channel->handlers, item, entry);
	rc = 0;

finally:
	return rc;
}

int niochannel_add_last(struct niochannel *channel, const char *name,
		struct niohandler *handler, void *obj)
{
	int rc;
	struct niohandler_item *item;

	if (!channel || !name || !handler) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("adding last: %s", name);

	if (niochannel_find(channel, name)) {
		nioerror("already exist: %s", name);
		rc = NIO_EEXIST;
		goto finally;
	}

	if (!(item = niohandler_new(name, handler, obj))) {
		nioerror("failed to create handler");
		rc = NIO_ENOMEM;
		goto finally;
	}

	TAILQ_INSERT_TAIL(&channel->handlers, item, entry);
	rc = 0;

finally:
	return rc;
}

int niochannel_add_before(struct niochannel *channel,
		const char *before, const char *name,
		struct niohandler *handler, void *obj)
{
	int rc;
	struct niohandler_item *match, *item;

	if (!channel || !before || !name || !handler) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("adding %s before %s", name, before);

	if (!(match = niochannel_find(channel, before))) {
		nioerror("not such handler: %s", before);
		rc = NIO_ENOENT;
		goto finally;
	}

	if (!(item = niohandler_new(name, handler, obj))) {
		nioerror("failed to create handler");
		rc = NIO_ENOMEM;
		goto finally;
	}

	TAILQ_INSERT_BEFORE(match, item, entry);
	rc = 0;

finally:
	return rc;
}

int niochannel_add_after(struct niochannel *channel,
		const char *after, const char *name,
		struct niohandler *handler, void *obj)
{
	int rc;
	struct niohandler_item *match, *item;

	if (!channel || !after || !name || !handler) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("adding %s after %s", name, after);

	if (!(match = niochannel_find(channel, after))) {
		nioerror("not such handler: %s", after);
		rc = NIO_ENOENT;
		goto finally;
	}

	if (!(item = niohandler_new(name, handler, obj))) {
		nioerror("failed to create handler");
		rc = NIO_ENOMEM;
		goto finally;
	}

	TAILQ_INSERT_AFTER(&channel->handlers, match, item, entry);
	rc = 0;

finally:
	return rc;
}

int niochannel_on_writable(struct niochannel *channel,
		niochannel_cb cb, void *cbarg)
{
	int rc;

	if (!channel || !cb) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	channel->cb = cb;
	channel->cbarg = cbarg;
	rc = 0;

finally:
	return rc;
}

int niochannel_write(struct niochannel *channel, void *message)
{
	int rc;

	if (!channel || !message) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (channel->handler &&
			channel->handler != TAILQ_FIRST(&channel->handlers)) {
		nioerror("channel pending");
		rc = NIO_EINPROGRESS;
		goto finally;
	}

	niodebug("writing message: %p", message);

	if ((rc = niochannel_demote(channel, NULL,
			TAILQ_LAST(&channel->handlers, niohandler_list),
			message))) {
		nioerror("failed to demote: %s", nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

#if 0
int niochannel_replace(struct niochannel *channel, const char *name,
		struct niohandler *handler, void *obj)
{
	int rc;
	struct niohandler_item *match, *item, *prev;

	if (!channel || !name || !handler) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("replacing handler: %s", name);

	if (!(match = niochannel_find(channel, name))) {
		nioerror("not such handler: %s", name);
		rc = NIO_ENOENT;
		goto finally;
	}

	if (!(item = niohandler_new(name, handler, obj))) {
		nioerror("failed to create handler");
		rc = NIO_ENOMEM;
		goto finally;
	}

	if ((rc = niohandler_setup(item))) {
		nioerror("failed to set up: %s", item->name);
		goto finally;
	}

	prev = TAILQ_PREV(match, niohandler_list, entry);
	TAILQ_REMOVE(&channel->handlers, match, entry);
	TAILQ_INSERT_TAIL(&channel->replaced, item, entry);

	if (prev) {
		TAILQ_INSERT_AFTER(&channel->handlers, prev, item, entry);
	} else {
		TAILQ_INSERT_HEAD(&channel->handlers, item, entry);
	}

	if (channel->handler == match) {
		switch (channel->direction) {
		case NIOCHANNEL_UPSTREAM:
			if ((rc = niochannel_promote(channel, prev, item,
					channel->message))) {
				nioerror("failed to re-send upstream: %s",
						strerror(rc));
				rc = NIO_EUNKNOWN;
				goto finally;
			}
			break;
		case NIOCHANNEL_DOWNSTREAM:
			if ((rc = niochannel_demote(channel,
					TAILQ_NEXT(match, entry),
					item, channel->message))) {
				nioerror("failed to re-send downstream: %s",
						strerror(rc));
				rc = NIO_EUNKNOWN;
				goto finally;
			}
			break;
		default:
			nioerror("unexpected direction: %d",
					channel->direction);
			rc = NIO_EUNKNOWN;
			goto finally;
		}
	}

	rc = 0;

finally:
	return rc;
}

int niochannel_move_before(struct niochannel *channel,
		const char *name, const char *before)
{
	int rc;
	struct niohandler_item *match, *item;

	if (!channel || !before || !name) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("moving '%s' before '%s'", name, before);

	if (!(match = niochannel_find(channel, before))) {
		nioerror("not such handler: %s", before);
		rc = NIO_ENOENT;
		goto finally;
	}

	if (!(item = niochannel_find(channel, name))) {
		nioerror("not such handler: %s", name);
		rc = NIO_ENOENT;
		goto finally;
	}

	TAILQ_REMOVE(&channel->handlers, item, entry);
	TAILQ_INSERT_BEFORE(match, item, entry);
	rc = 0;

finally:
	return rc;
}

int niochannel_move_after(struct niochannel *channel,
		const char *name, const char *after)
{
	int rc;
	struct niohandler_item *match, *item;

	if (!channel || !after || !name) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	niodebug("moving '%s' after '%s'", name, after);

	if (!(match = niochannel_find(channel, after))) {
		nioerror("not such handler: %s", after);
		rc = NIO_ENOENT;
		goto finally;
	}

	if (!(item = niochannel_find(channel, name))) {
		nioerror("not such handler: %s", name);
		rc = NIO_ENOENT;
		goto finally;
	}

	TAILQ_REMOVE(&channel->handlers, item, entry);
	TAILQ_INSERT_AFTER(&channel->handlers, match, item, entry);
	rc = 0;

finally:
	return rc;
}
#endif

struct niohandler_item *niochannel_find(
		struct niochannel *channel, const char *name)
{
	struct niohandler_item *item;

	TAILQ_FOREACH(item, &channel->handlers, entry) {
		if (!name) {
			if (!item->name) {
				return item;
			}
			continue;
		}

		if (!(strcmp(item->name, name))) {
			return item;
		}
	}

	return NULL;
}

int niochannel_open(struct niochannel *channel,
		struct niosocket *socket, struct timeval *timeout,
		niochannel_close_cb cb, void *cbarg)
{
	int rc;

	if (!channel || !socket) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (channel->socket) {
		nioerror("already open");
		rc = NIO_EPERM;
		goto finally;
	}

	niodebug("opening %s:%d (timeout=%ld.%06ld)",
			inet_ntoa(socket->address), socket->port,
			timeout ? timeout->tv_sec : 0,
			timeout ? timeout->tv_usec : 0);

	if (TAILQ_EMPTY(&channel->handlers)) {
		nioerror("empty channel(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (socket->descriptor == INVALID_SOCKET) {
		nioerror("invalid socket");
		rc = NIO_ECLOSED;
		goto finally;
	}

	niodebug("invoking %p to create buffer", channel->factory.create);

	if (!(channel->buffer = (*channel->factory.create)(
			channel, channel->factory_arg))) {
		nioerror("failed to create buffer");
		rc = NIO_EUNKNOWN;
		goto finally;
	}

	if ((rc = niochannel_setup(channel))) {
		nioerror("failed to set up: %s",
				strerror(rc));
		rc = NIO_EUNKNOWN;
	}

	channel->socket = socket;
	channel->close_cb = cb;
	channel->close_cbarg = cbarg;

	if ((rc = niochannel_promote(channel, NULL,
			TAILQ_FIRST(&channel->handlers), channel->buffer))) {
		nioerror("failed to promote: %s", nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	if (rc != 0) {
		if (channel->buffer) {
			(*channel->factory.release)(
					channel->buffer, channel->factory_arg);
			channel->buffer = NULL;
		}
		channel->socket = NULL;
	}
	return rc;
}

int niochannel_close(struct niochannel *channel)
{
	int rc;
	struct niosocket *socket = channel->socket;
	niochannel_close_cb cb = channel->close_cb;
	void *cbarg = channel->close_cbarg;

	if (!(channel->socket)) {
		nioerror("not open");
		rc = NIO_ECLOSED;
		goto finally;
	}

	niodebug("closing %s:%d", inet_ntoa(socket->address), socket->port);

	if ((rc = niochannel_teardown(channel))) {
		nioerror("failed to tear down: %s", strerror(rc));
		rc = NIO_EUNKNOWN;
		goto finally;
	}

	niodebug("invoking %p to release buffer", channel->factory.release);

	(*channel->factory.release)(channel->buffer, channel->factory_arg);

	channel->socket = NULL;

	if (cb) {
		niodebug("invoking %p to handle close", cb);

		(*cb)(channel, socket, cbarg);
	} else {
		niowarn("no close callback; risking connection "
				"and memory leakage");
	}

finally:
	return rc;
}

int niochannel_promote(struct niochannel *channel, struct niohandler_item *from,
		struct niohandler_item *to, void *message)
{
	int rc;

	niodebug("promoting from '%s' to '%s'", from ? from->name : "(null)",
			to ? to->name : "(null)");

	channel->direction = NIOCHANNEL_UPSTREAM;
	channel->handler = to;
	channel->message = message;

	niodebug("invoking %p to handle upstream: %s", to->ops.handle_upstream,
			to->name);

	if ((rc = (*to->ops.handle_upstream)(to->obj, channel, message))) {
		nioerror("failed to handle event: %s", strerror(rc));
		goto finally;
	}

//	if (!channel->handler) {
//		niobuffer_clear(channel->buffer);
//
//		if (channel->keep_alive) {
//			if ((rc = niochannel_finalize(channel))) {
//				nioerror("failed to finalize: %s",
//						strerror(rc));
//				rc = NIO_EUNKNOWN;
//			}
//
//			niodebug("re-activating channel");
//
//			if ((rc = niochannel_promote(channel, NULL,
//					TAILQ_FIRST(&channel->handlers),
//					channel->buffer))) {
//				nioerror("failed to promote: %s",
//						nio_strerror(rc));
//				goto finally;
//			}
//		} else {
//			niochannel_close(channel);
//		}
//	}

	rc = 0;

finally:
	return rc;
}

int niochannel_send_upstream(struct niochannel *channel, const char *name,
		void *message)
{
	int rc;
	struct niohandler_item *item, *next;

	if (!channel) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (!(item = niochannel_find(channel, name))) {
		nioerror("no such handler: %s", name);
		rc = NIO_EINVAL;
		goto finally;
	}

	next = TAILQ_NEXT(item, entry);

	if ((rc = niochannel_promote(channel, item, next, message))) {
		nioerror("failed to promote: %s", nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

int niochannel_demote(struct niochannel *channel, struct niohandler_item *from,
		struct niohandler_item *to, void *message)
{
	int rc;

	niodebug("demoting from '%s' to '%s'", from ? from->name : "(null)",
			to ? to->name : "(null)");

	channel->direction = NIOCHANNEL_DOWNSTREAM;
	channel->handler = to;
	channel->message = message;

	if (to) {
		niodebug("invoking %p to handle downstream: %s",
				to->ops.handle_downstream, to->name);

		if ((rc = (*to->ops.handle_downstream)(to->obj,
				channel, message))) {
			nioerror("failed to handle event: %s", strerror(rc));
			goto finally;
		}
	} else {
		niobuffer_clear(channel->buffer);

		if (message && channel->keep_alive) {
			if ((rc = niochannel_teardown(channel))) {
				nioerror("failed to tear down: %s",
						strerror(rc));
				rc = NIO_EUNKNOWN;
			}

			if ((rc = niochannel_setup(channel))) {
				nioerror("failed to set up: %s",
						strerror(rc));
				rc = NIO_EUNKNOWN;
			}

			niodebug("re-activating channel");

			if ((rc = niochannel_promote(channel, NULL,
					TAILQ_FIRST(&channel->handlers),
					channel->buffer))) {
				nioerror("failed to promote: %s",
						nio_strerror(rc));
				goto finally;
			}

			if (channel->cb) {
				niodebug("invoking %p to notify writable",
						channel->cb);
				(*channel->cb)(channel, channel->cbarg);
			}
		} else {
			niochannel_close(channel);
		}
	}

	rc = 0;

finally:
	return rc;
}

int niochannel_send_downstream(struct niochannel *channel, const char *name,
		void *message)
{
	int rc;
	struct niohandler_item *item, *prev;

	if (!channel) {
		nioerror("null argument(s)");
		rc = NIO_EINVAL;
		goto finally;
	}

	if (!(item = niochannel_find(channel, name))) {
		nioerror("no such handler: %s", name);
		rc = NIO_EINVAL;
		goto finally;
	}

	prev = TAILQ_PREV(item, niohandler_list, entry);

	if ((rc = niochannel_demote(channel, item, prev, message))) {
		nioerror("failed to demote: %s", nio_strerror(rc));
		goto finally;
	}

	rc = 0;

finally:
	return rc;
}

int niochannel_setup(struct niochannel *channel)
{
	int rc;
	struct niohandler_item *item;

	niodebug("setting up all handlers");

	TAILQ_FOREACH(item, &channel->handlers, entry) {
		if ((rc = niohandler_setup(item))) {
			nioerror("failed to set up: %s", item->name);
			goto finally;
		}
	}

	TAILQ_FOREACH(item, &channel->replaced, entry) {
		if ((rc = niohandler_setup(item))) {
			nioerror("failed to set up: %s", item->name);
			goto finally;
		}
	}

	rc = 0;

finally:
	return rc;
}

int niochannel_teardown(struct niochannel *channel)
{
	int rc;
	struct niohandler_item *item;

	niodebug("tearing down all handlers");

	TAILQ_FOREACH(item, &channel->replaced, entry) {
		if ((rc = niohandler_teardown(item))) {
			nioerror("failed to tear down: %s", item->name);
			goto finally;
		}
	}

	TAILQ_FOREACH(item, &channel->handlers, entry) {
		if ((rc = niohandler_teardown(item))) {
			nioerror("failed to tear down: %s", item->name);
			goto finally;
		}
	}

	rc = 0;

finally:
	return rc;
}
